using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Xml;
using System.Net;
using System.Text;
using System.IO;
using System.Threading;
using System.Collections;
using System.Collections.Specialized;
using System.Collections.Generic;
using Rest.Transport;
using Rest.Xml;
using System.Diagnostics;

namespace Rest
{
    /// <summary>
    /// Object used to send requests to a REST web service
    /// </summary>
    /// <remarks>
    /// Instances can be configured to use different HTTP verbs by supplying a
    /// RestTransport object.
    /// </remarks>
    /// <example>
    /// <code>
    ///  RestCommand restCommand = new RestCommand(
    ///     "http://api.local.yahoo.com/MapsService/V1/geocode");
    ///     
    ///  restCommand.RestTransport = new GetRestTransport();
    ///  restCommand.Parameters.Add(new RestParameter("appid", "YahooDemo"));
    ///  restCommand.Parameters.Add(new RestParameter("street", "703 First Street"));
    ///  restCommand.Parameters.Add(new RestParameter("city", "Sunnyvale"));
    ///  restCommand.Parameters.Add(new RestParameter("state", "CA"));
    ///   
    ///  return restCommand.ExecuteXml();
    /// </code>
    /// </example>
    public class RestCommand
    {
        #region Fields and Properties

        private Boolean allowAutoRedirect = true;
        private CookieCollection cookies;
        private ICredentials credentials;
        private RestDataItemCollection data;
        private WebHeaderCollection headers;
        private RestParameterCollection parameters;
        private Int32 redirectCount = 0;
        private RestRedirectionPolicy redirectionPolicy =
            RestRedirectionPolicy.DefaultRestRedirectionPolicy;
        private Uri serviceUri;
        private TimeSpan timeout = new TimeSpan(0, 0, 0, 30, 0);
        private RestTransport transport;

        /// <summary>
        /// When set to true, redirect headers are followed. Defaults to true.
        /// </summary>
        public Boolean AllowAutoRedirect
        {
            get { return allowAutoRedirect; }
            set { allowAutoRedirect = value; }
        }

        /// <summary>
        /// Cookies associated with the current request
        /// </summary>
        public CookieCollection Cookies
        {
            get
            {
                return cookies ?? (cookies = new CookieCollection());
            }
        }

        /// <summary>
        /// Credentials associated with current command
        /// </summary>
        public ICredentials Credentials
        {
            get { return credentials; }
            set { credentials = value; }
        }

        /// <summary>
        /// List of RestDataItem items to be added to transports supporting 
        /// sending data
        /// </summary>
        public RestDataItemCollection Data
        {
            get
            {
                return data ?? (data = new RestDataItemCollection());
            }
        }

        /// <summary>
        /// Headers collection for the REST command. 
        /// These are stored until the command is executed.
        /// </summary>
        public WebHeaderCollection Headers
        {
            get
            {
                return headers ?? (headers = new WebHeaderCollection());
            }
        }

        /// <summary>
        /// Set of parameters
        /// </summary>
        public RestParameterCollection Parameters
        {
            get
            {
                return parameters ?? (parameters = new RestParameterCollection());
            }
            set { parameters = value; }
        }

        /// <summary>
        /// The RestRedirectionPolicy policy to use
        /// </summary>
        public RestRedirectionPolicy RedirectionPolicy
        {
            get { return redirectionPolicy; }
            set { redirectionPolicy = value; }
        }

        /// <summary>
        /// The URI of the service
        /// </summary>
        public Uri ServiceUri
        {
            get { return serviceUri; }
            set { serviceUri = value; }
        }

        /// <summary>
        /// Command connection timeout (milliseconds), default to 30s
        /// </summary>
        public TimeSpan Timeout
        {
            get { return timeout; }
            set { timeout = value; }
        }

        /// <summary>
        /// The rest transport
        /// </summary>
        public RestTransport Transport
        {
            get { return transport; }
            set { transport = value; }
        }

        /// <summary>
        /// Raised just before execution
        /// </summary>
        public event EventHandler<RestCommandEventArgs> Executing;

        /// <summary>
        /// Raised whenever the command encounters Response headers requiring
        /// redirect.
        /// TODO: Implement event args
        /// </summary>
        public event EventHandler<RestCommandEventArgs> Redirecting;

        #endregion

        #region ctor

        /// <summary>
        /// ctor
        /// </summary>
        public RestCommand() { }

        /// <summary>
        /// ctor : Uri
        /// </summary>
        /// <param name="serviceUri">The URI for the service</param>
        public RestCommand(Uri serviceUri)
        {
            this.serviceUri = serviceUri;
        }

        /// <summary>
        /// ctor : String
        /// </summary>
        /// <param name="serviceUrl">The URL of the service</param>
        /// <exception cref="UriFormatException" />
        public RestCommand(String serviceUrl)
        {
            this.serviceUri = new Uri(serviceUrl);
        }

        /// <summary>
        /// ctor: Uri, RestTransport
        /// </summary>
        /// <param name="serviceUri"></param>
        /// <param name="transport"></param>
        public RestCommand(Uri serviceUri, RestTransport transport) :
            this(serviceUri)
        {
            this.transport = transport;
        }

        /// <summary>
        /// ctor: Uri, RestTransport
        /// </summary>
        /// <param name="serviceUrl">The URL string of the service</param>
        /// <param name="transport"></param>
        public RestCommand(String serviceUrl, RestTransport transport) :
            this(serviceUrl)
        {
            this.transport = transport;
        }


        #endregion

        /// <summary>
        /// Loads the Response XML
        /// </summary>
        /// <exception cref="WebException" />
        /// <exception cref="XmlException " />
        private XmlDocument LoadResponseXml(Stream stream)
        {
            XmlDocument responseXmlDocument = new XmlDocument();
            responseXmlDocument.Load(stream);
            return responseXmlDocument;
        }

        /// <summary>
        /// Throws a RestException based on the supplied string.
        /// </summary>
        /// <exception cref="RestServiceException" />
        private void ThrowRestException(String message)
        {
            ThrowRestException(message, null);
        }

        /// <summary>
        /// Throws a RestServiceException based on the supplied string.
        /// This method is defined so members can throw exceptions and remain 
        /// eligible for in-lining.
        /// </summary>
        /// <exception cref="RestServiceException" />
        private void ThrowRestException(String message, Exception innerException)
        {
            throw new RestException(message, innerException);
        }

        /// <summary>
        /// Raises Executing event
        /// </summary>
        /// <param name="e"></param>
        protected void OnExecuting(RestCommandEventArgs e)
        {
            EventHandler<RestCommandEventArgs> handler = Executing;

            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// Raises Redirecting event
        /// </summary>
        protected void OnRedirecting(RestCommandEventArgs e)
        {
            EventHandler<RestCommandEventArgs> handler = Redirecting;

            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// Redirects the current execution, preserving headers and body
        /// TODO: Does not handle cases where the Location header 
        /// is of the form "/some-page.html", i.e. not fully-qualified.
        /// </summary>
        /// <param name="httpResponse"></param>
        /// <exception cref="RestException" />
        public HttpWebResponse Redirect(HttpWebResponse httpResponse)
        {
            if (null == httpResponse.Headers["Location"])
            {
                ThrowRestException("Unable to redirect " +
                    "because there was no \"Location\" header " +
                    "received"
                );
            }

            if (redirectCount++ > RedirectionPolicy.RedirectLimit)
            {
                ThrowRestException(
                    String.Format(
                        "Encountered too many redirects. " +
                        "Limit has been set at {0}.",
                        RedirectionPolicy.RedirectLimit
                    )
                );
            }

            String locationValue = httpResponse.Headers["Location"];

            // Check if new location is relative
            if (locationValue.StartsWith("/"))
            {
                locationValue = String.Format(
                    "{0}://{1}{2}",
                    httpResponse.ResponseUri.Scheme,
                    httpResponse.ResponseUri.Host,
                    locationValue
                );
            }

            this.ServiceUri = new Uri(locationValue);

            return Execute();
        }

        /// <summary>
        /// Returns a stream reader for the current command
        /// </summary> 
        /// <returns></returns>
        public StreamReader ExecuteStreamReader()
        {
            return new StreamReader(GetResponseStream());
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public String ExecuteText() 
        {
            HttpWebResponse response = Execute();

            return GetResponseText(response); 
        }

        /// <summary>
        /// Executes command and returns result as an XML document. 
        /// </summary>
        /// <remarks>
        /// This is the primary execution mechanism, given that these services
        /// communicate via XML.
        /// </remarks>
        /// <returns>An XmlDocument containing the Response from the service</returns>
        /// <exception cref="RestServiceException" />
        /// <exception cref="XmlException">
        /// May occur if Response cannot be parsed as XML</exception>
        public XmlDocument ExecuteXml()
        {
            Stream stream = null;

            try
            {
                stream = GetResponseStream();
                return LoadResponseXml(stream);
            }
            catch (XmlException xmlEx)
            {
                ThrowRestException("Unable to open request. " +
                    xmlEx.Message, xmlEx);
            }
            finally
            {
                if (stream != null)
                {
                    stream.Close();
                }
            }
            return null;
        }

        /// <summary>
        /// Sends a request and returns the response stream
        /// </summary>
        /// <returns></returns>
        /// <exception cref="InvalidOperationException" />
        public Stream GetResponseStream()
        {
            // ExecuteXml request and return the stream
            // BJB, 01-12-2006 : This can throw exceptions based on the value of the
            // Response headers. In this case, use the Response and Status 
            // properties of the exception to determine the Response from the server
            // See: ms-help://MS.VSCC.v80/MS.MSDN.v80/MS.NETDEVFX.v20.en/
            //     cpref10/html/M_System_Net_HttpWebRequest_GetResponse.htm
            return Execute().GetResponseStream();
        }

        /// <summary>
        /// Begin asynchronous execution, returning an ExecuteAsyncResult 
        /// representing the state of the operation.
        /// </summary>
        /// <returns></returns>
        public ExecuteAsyncResult BeginExecute(
            AsyncCallback callback,
            Object asyncState)
        {
            ExecuteAsyncResult result = new ExecuteAsyncResult(callback, asyncState);

            ThreadPool.QueueUserWorkItem(
                new WaitCallback(BeginExecuteCore), result);

            return result;
        }

        /// <summary>
        /// Internal method to start async operation
        /// </summary>
        /// <param name="asyncResult"></param>
        /// <exception cref="ArgumentNullException">
        /// Parameter <paramref name="asyncResult"/> cannot be null
        /// </exception>
        private void BeginExecuteCore(Object asyncResult)
        {
            if (null == asyncResult)
            {
                throw new ArgumentNullException("asyncResult");
            }

            ExecuteAsyncResult executeAsyncResult =
                (ExecuteAsyncResult)asyncResult;

            HttpWebResponse response = null;
            Exception exception = null;

            try
            {
                response = Execute();
            }
            catch (Exception e)
            {
                exception = e;
            }

            executeAsyncResult.SetAsCompleted(response, exception, false);
        }


        /// <summary>
        /// Ends the asynchronous execute operation identified by
        /// <paramref name="asyncResult"/>
        /// </summary>
        /// <param name="asyncResult"></param>
        public HttpWebResponse EndExecute(IAsyncResult asyncResult)
        {
            ExecuteAsyncResult executeResult = (ExecuteAsyncResult)asyncResult;

            // Ensure the operation is complete by blocking
            if (false == executeResult.IsCompleted)
            {
                executeResult.EndInvoke();
            }

            return executeResult.Response;
        }

        /// <summary>
        /// Executes a request and returns the Response
        /// </summary>
        /// <remarks>
        /// BJB, 18-12-2006 : It is extremely important to allow
        /// RestTransport derived types to only write to the request
        /// stream after all headers have been set, otherwise they will
        /// not be included in the message.
        /// </remarks> 
        /// <returns></returns>
        /// <exception cref="System.Net.WebException"/>
        public HttpWebResponse Execute()
        {
            RestCommandEventArgs executeEventArgs;
            OnExecuting(executeEventArgs = new RestCommandEventArgs());

            if (executeEventArgs.Cancel)
            {
                return null; // Event has been cancelled
            }

            if (null == ServiceUri)
            {
                throw new InvalidOperationException("ServiceUri is unset");
            }

            if (null == Transport)
            {
                throw new InvalidOperationException("Transport is unset");
            }

            //
            // Create and configure the request
            // 
            HttpWebRequest request = Transport.CreateRequest(ServiceUri, Parameters);

            if (Credentials != null)
                request.Credentials = this.Credentials;

            //
            // Set request properties
            //
            request.Timeout = Convert.ToInt32(Timeout.TotalMilliseconds);

            // This class handles redirects to ensure all parameters are preserved
            request.AllowAutoRedirect = false;
            request.Method = Transport.HttpMethod;
            request.ContentType = Transport.HttpContentType.ContentType;

            //
            // Add custom headers
            //
            if (Headers != null && Headers.Count > 0)
            {  
                request.Headers.Add(Headers);
            }

            request.CookieContainer = new CookieContainer();

            if (Cookies != null && Cookies.Count > 0)
            {
                request.CookieContainer.Add(Cookies);
            }

            //
            // Write the body
            //
            if (Transport.SupportsBody)
            {
                // BJB, 18-12-2006 : Here we're forced to buffer the body contents
                // so we can set the Content-length header which is required. 
                // the content-length header MUST be set before request stream is written to.
                //
                // TODO, BJB, 26-02-2007 : According to Microsoft, the Byte array used by
                // MemoryStream resizes itself by doubling, which can lead to poor memory 
                // performance. Consider refactoring the MemoryStream declared 
                // here to be of fixed size. Objects > 85k are added to the 
                // Large Object Heap (LOH), and also to GC generation 2.
                using (MemoryStream memStream = new MemoryStream(1024))
                {
                    Transport.WriteRequestBody(memStream, Parameters, Data,
                        Encoding.UTF8);
                    request.ContentLength = memStream.Length;
                    memStream.WriteTo(request.GetRequestStream());
                }
            }

            //
            // Execute the request
            //
            HttpWebResponse httpResponse = null;

            try
            {
                httpResponse = (HttpWebResponse)request.GetResponse();
            }
            catch (WebException e)
            {
                // Some Http response status codes are converted to exceptions
                // when using HttpWebRequest.
                httpResponse = (HttpWebResponse)e.Response;
            }

            //
            // Handle redirects, ensuring the current execution
            // 
            if (AllowAutoRedirect)
            {
                if (
                    httpResponse.StatusCode == HttpStatusCode.Redirect ||
                    httpResponse.StatusCode == HttpStatusCode.Found ||
                    httpResponse.StatusCode == HttpStatusCode.RedirectKeepVerb ||
                    httpResponse.StatusCode == HttpStatusCode.SeeOther
                )
                {

                    if (httpResponse.StatusCode == HttpStatusCode.Redirect)
                    {
                        if (false == this.RedirectionPolicy.PreserveHttpTransportOnRedirect)
                        {
                            Transport = RestTransport.Get;
                        }
                    }

                    RestCommandEventArgs e;
                    OnRedirecting(e = new RestCommandEventArgs(httpResponse));

                    if (false == e.Cancel)
                    {
                        return Redirect(httpResponse);
                    }
                }
            }

            redirectCount = 0; 
            return httpResponse;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="request"></param>
        /// <param name="headers"></param>
        static void ApplyHeaders(
            WebRequest request, 
            WebHeaderCollection headers
        )
        {
            request.Headers.Add(headers);
        }

        # region * DSL * 

        /// <summary>
        /// 
        /// </summary>
        /// <param name="name"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public RestCommand WithParameter(String name, Object value)
        {
            Parameters.Add(name, value.ToString());
            return this;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="transport"></param>
        /// <returns></returns>
        public RestCommand With(RestTransport transport)
        {
            Transport = transport;
            return this;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="transport"></param>
        /// <returns></returns>
        public RestCommand With(RestRedirectionPolicy redirectionPolicy)
        {
            this.redirectionPolicy = redirectionPolicy;
            return this;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="serviceUri"></param>
        /// <returns></returns>
        public RestCommand On(Uri serviceUri)
        {
            this.serviceUri = serviceUri;
            return this;
        }

        #endregion

        /// <summary>
        /// 
        /// </summary>
        /// <param name="response"></param>
        /// <returns></returns>
        public static String GetResponseText(WebResponse response)
        {
            if (null == response)
                throw new ArgumentNullException("response");

            String contentLengthHeader = response.Headers["Content-length"];

            if (contentLengthHeader != null)
            {
                Int32 length;

                Int32.TryParse(contentLengthHeader, out length);

                Debug.WriteLine(
                    String.Format(
                        "Url: {0}, length: {1} B", 
                        response.ResponseUri.AbsoluteUri, 
                        length
                    )
                ); 
            }

            Int32 linesRead = 0;

            using (TextReader reader = new StreamReader(response.GetResponseStream()))
            using (StringWriter writer = new StringWriter())
            {
                String line = null;
                
                while ((line = reader.ReadLine()) != null)
                {
                    writer.WriteLine(line);
                    linesRead++;
                }

                Debug.WriteLine("(" + linesRead + ") lines read.");

                return writer.GetStringBuilder().ToString();
            }
        }
    }
}